// Copyright 2010 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package com.google.api.adwords.lib;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Map;
import java.util.Properties;

import javax.xml.rpc.ServiceException;

/**
 * The {@code AdWordsUser} class contains all needed information to
 * authenticate a user as well as generate a valid service for that user. The
 * way in which a service is generated is performed as:
 * <p>
 * <code>
 * AdWordsUser user = new AdWordsUser(email, password, clientId, userAgent,
 *     developerToken, applicationToken);
 * ServiceInterface service = user.getService(AdWordsService.version.serviceName);
 * </code>
 * <p>
 * The interface {@code <Service>Interface} should match that described by
 * {@code AdWordsService.version.serviceName} or else a
 * {@code ServiceException} will be thrown. For example, if you wished to
 * get the service {@code CampaignService} and you referenced this service by
 * {@code AdWordsService.v200909.CAMAPAIGN_SERVICE}, your import of
 * {@code CampaignServiceInterface} should be from the package
 * {@code com.google.api.adwords.v200909.cm}.
 *
 * @author api.arogal@gmail.com (Adam Rogal)
 */
public class AdWordsUser {
  // Defaults for lib name and version, both of which will be loaded from
  // the build.properties file.
  private final static String DEFAULT_LIB_PREFIX_NAME = "AdWords API";
  private final static String DEFAULT_LIB_POSTFIX_NAME = "Java Client Library";
  private final static String DEFAULT_LIB_VERSION = "";

  private final static String DEFAULT_PROPERTY_FILE_LOCATION =
      System.getProperty("user.home") + "/adwords.properties";

  private static String libName;
  private static String libVersion;

  private static final String USE_CLASSPATH_CLIENT_CONFIG_PROPERTY =
      "com.google.api.adwords.lib.useclasspathclientconfig";
  private static boolean useClasspathClientConfig = false;

  private static final String USE_GAE_HTTP_API_PROPERTY =
      "com.google.api.adwords.lib.usegaehttpapi";
  private static boolean useGaeHttpApi = true;

  /**
   * Load properties statically.
   */
  static {
    loadBuildProperties();
    loadSystemProperties();
  }

  /** @deprecated Use {@code AdWordsService.V13.ACCOUNT_SERVICE} instead. */
  @Deprecated
  public static final AdWordsService ACCOUNT_SERVICE = AdWordsService.V13.ACCOUNT_SERVICE;


  /** @deprecated Use {@code AdWordsService.V13.REPORT_SERVICE} instead. */
  @Deprecated
  public static final AdWordsService REPORT_SERVICE = AdWordsService.V13.REPORT_SERVICE;

  /**
   * @deprecated Use {@code AdWordsService.V13.TRAFFIC_ESTIMATOR_SERVICE}
   *     instead.
   */
  @Deprecated
  public static final AdWordsService TRAFFIC_ESTIMATOR_SERVICE =
      AdWordsService.V13.TRAFFIC_ESTIMATOR_SERVICE;

  private final String email;
  private final String password;
  private final String clientEmail;
  private final String clientCustomerId;
  private final String userAgent;
  private final String developerToken;


  /**
   * @deprecated Application tokens no longer required
   */
  @Deprecated
  private final String applicationToken;

  private String authToken = null;

  private boolean useSandbox = false;
  private boolean partialFailure = false;
  private boolean returnMoneyInMicros = false;

  /**
   * @param email the primary email address of the user
   *     (either the MCC or client account)
   * @param password the password of the primary email
   * @param clientId the id of the client to use. Can be a numeric ID, i.e.
   *     123-456-7890, or an email address, i.e user@gmail.com. Can
   *     be left {@code null} to only use the MCC
   * @param userAgent the user-agent
   * @param developerToken the developer token
   * @param useSandbox {@code true} if the Sandbox should be used
   */
  public AdWordsUser(String email, String password, String clientId,
      String userAgent, String developerToken, boolean useSandbox) {
    this.email = email;
    this.password = password;
    this.developerToken = developerToken;
    this.userAgent = userAgent;
    this.useSandbox = useSandbox;
    this.applicationToken = null;

    if (clientId != null) {
      if (isEmail(clientId)) {
        this.clientEmail = clientId;
        this.clientCustomerId = null;
      } else {
        this.clientEmail = null;
        this.clientCustomerId = clientId;
      }
    } else {
      this.clientCustomerId = null;
      this.clientEmail = null;
    }
  }

  /**
   * @param email the primary email address of the user
   *     (either the MCC or client account)
   * @param password the password of the primary email
   * @param clientId the id of the client to use. Can be a numeric ID, i.e.
   *     123-456-7890, or an email address, i.e user@gmail.com. Can
   *     be left {@code null} to only use the MCC
   * @param userAgent the user-agent
   * @param developerToken the developer token
   * @param applicationToken the application token
   * @param useSandbox {@code true} if the Sandbox should be used
   * @deprecated Application tokens are no longer required
   */
  @Deprecated
  public AdWordsUser(String email, String password, String clientId,
      String userAgent, String developerToken,
      String applicationToken, boolean useSandbox) {
    this.email = email;
    this.password = password;
    this.developerToken = developerToken;
    this.userAgent = userAgent;
    this.useSandbox = useSandbox;
    this.applicationToken = applicationToken;

    if (clientId != null) {
      if (isEmail(clientId)) {
        this.clientEmail = clientId;
        this.clientCustomerId = null;
      } else {
        this.clientEmail = null;
        this.clientCustomerId = clientId;
      }
    } else {
      this.clientCustomerId = null;
      this.clientEmail = null;
    }
  }

  /**
   * Constructor defaults to using the production environment.
   *
   * @param email the primary email address of the user
   *     (either the MCC or client account)
   * @param password the password of the primary email
   * @param clientId the id of the client to use. Can be a numeric ID, i.e.
   *     123-456-7890, or an email address, i.e user@gmail.com. Can
   *     be left {@code null} to only use the MCC
   * @param userAgent the user-agent
   * @param developerToken the developer token
   * @param applicationToken the application token
   * @deprecated Application tokens no longer required
   */
  @Deprecated
  public AdWordsUser(String email, String password, String clientId, String userAgent,
      String developerToken, String applicationToken) {
    this(email, password, clientId, userAgent, developerToken, applicationToken, false);
  }

  /**
   * Constructor defaults to using the production environment.
   *
   * @param email the primary email address of the user
   *     (either the MCC or client account)
   * @param password the password of the primary email
   * @param clientId the id of the client to use. Can be a numeric ID, i.e.
   *     123-456-7890, or an email address, i.e user@gmail.com. Can
   *     be left {@code null} to only use the MCC
   * @param userAgent the user-agent
   * @param developerToken the developer token
   */
  public AdWordsUser(String email, String password, String clientId, String userAgent,
      String developerToken) {
    this(email, password, clientId, userAgent, developerToken, false);
  }

  /**
   * Constructor defaults to using the production environment and using the
   * {@code email} field as the effective user.
   *
   * @param email the primary email address of the user (either the MCC or
   *     client account)
   * @param password the password of the primary email
   * @param userAgent the user-agent
   * @param developerToken the developer token
   */
  public AdWordsUser(String email, String password, String userAgent, String developerToken) {
    this(email, password, null, userAgent, developerToken, false);
  }

  /**
   * Creates {@code AdWordsUser} with default properties loaded from
   * "~/adwords.properties" file.
   *
   * @throws IOException if problem occurred while reading file
   */
  public AdWordsUser() throws IOException {
    this(DEFAULT_PROPERTY_FILE_LOCATION);
  }

  /**
   * Creates {@code AdWordsUser} with properties loaded from given file.
   *
   * @param fileName absolute or relative path to the properties file, which
   *     has to follow {@code java.util.Properties} file format
   * @throws IOException if problem occurred while reading file
   */
  public AdWordsUser(String fileName) throws IOException {
    this(getMap(fileName));
  }

  /**
   * Creates {@code AdWordsUser} with properties from given
   * {@code Map}.
   * <p>
   * The {@code Map} may contain email, password, clientId, useragent,
   * developerToken, applicationToken, and useSandbox.
   *
   * @param properties the map object containing the constructor parameters
   */
  public AdWordsUser(Map properties) {
    this((String) properties.get("email"),
         (String) properties.get("password"),
         getClientIdFromProperties(properties),
         (String) properties.get("useragent"),
         (String) properties.get("developerToken"),
         (String) properties.get("applicationToken"),
         (properties.get("useSandbox") == null) ? false
             : ((String) properties.get("useSandbox")).equalsIgnoreCase("true"));
    // Check for backwards compatibility.
    if (properties.get("alternateUrl") != null
        && properties.get("alternateUrl").equals("https://sandbox.google.com/")) {
      this.useSandbox = true;
    }
    this.partialFailure = (properties.get("partialFailure") == null) ? false
        : ((String) properties.get("partialFailure")).equalsIgnoreCase("true");
    this.returnMoneyInMicros = (properties.get("returnMoneyInMicros") == null) ? false
        : ((String) properties.get("returnMoneyInMicros")).equalsIgnoreCase("true");
  }

  /**
   * Gets an instantiated service generated for this user determined by
   * {@code service}, which will only perform validation. Should be used in the
   * form:
   * <code>
   * ServiceNameInterface service = user.getService(AdWordsService.Version.Service);
   * </code>
   *
   * where ServiceNameInterface is described in the javadoc within
   * AdWordsService.Version.Service, and both Version and Service are constants.
   * <p>
   * The result of this call will either be an empty response or a SOAP
   * exception with the API errors causing the fault.
   *
   * @param <T> the type determined by the return-capture variable to cast the
   *     generated stub to.
   * @param service the {@code AdWordsService} used to generate
   *     the service
   * @return the instantiated service for this user
   * @throws ServiceException if service stub could not be generated or
   *     if the return-capture variable's type does not match the service
   *     requested
   */
  public <T extends java.rmi.Remote> T getValidationService(AdWordsService service)
      throws ServiceException {
    try {
      return (T) AdWordsServiceFactory.generateServiceStub(service, this,
          service.getEndpointServer(this.isUsingSandbox()), true);
    } catch (ClassCastException e) {
      throw new ServiceException("Cannot cast service. "
          + "Check the type of return-capture variable.", e);
    }
  }

  /**
   * Gets an instantiated service generated for this user determined by
   * {@code service}. Should be used in the form:
   * <code>
   * ServiceNameInterface service = user.getService(AdWordsService.Version.Service);
   * </code>
   *
   * where ServiceNameInterface is described in the javadoc within
   * AdWordsService.Version.Service, and both Version and Service are constants.
   *
   * @param <T> the type determined by the return-capture variable to cast the
   *     generated stub to.
   * @param service the {@code AdWordsService} used to generate
   *     the service
   * @return the instantiated service for this user
   * @throws ServiceException if service stub could not be generated or
   *     if the return-capture variable's type does not match the service
   *     requested
   */
  public <T extends java.rmi.Remote> T getService(AdWordsService service)
      throws ServiceException {
    try {
      return (T) AdWordsServiceFactory.generateServiceStub(service, this,
          service.getEndpointServer(this.isUsingSandbox()), false);
    } catch (ClassCastException e) {
      throw new ServiceException("Cannot cast service. Check the type of return-capture variable.",
          e);
    }
  }

  /**
   * Gets an instantiated service generated for this user determined by
   * {@code service}. Should be used in the form:
   * <code>
   * ServiceNameInterface service = user.getService(AdWordsService.Version.Service);
   * </code>
   *
   * where ServiceNameInterface is described in the javadoc within
   * AdWordsService.Version.Service, and both Version and Service are constants.
   *
   * @param <T> the type determined by the return-capture variable to cast the
   *     generated stub to.
   * @param service the {@code AdWordsService} used to generate
   *     the service
   * @param endpointServer the endpoint server of the service
   * @return the instantiated service for this user
   * @throws ServiceException if service stub could not be generated or
   *     if the return-capture variable's type does not match the service
   *     requested
   */
  public <T extends java.rmi.Remote> T getService(AdWordsService service, String endpointServer)
      throws ServiceException {
    try {
      return (T) AdWordsServiceFactory.generateServiceStub(service, this, endpointServer, false);
    } catch (ClassCastException e) {
      throw new ServiceException("Cannot cast service. Check the type of return-capture variable.",
          e);
    }
  }

  /**
   * Creates a new {@code AdWordsUser} client of this {@code AdWordsUser} by
   * replacing the current {@code clientId} with the one specified. This method
   * does not replace the current MCC, and the user assumes that the MCC
   * denoted in the original {@code AdWordsUser} has access to the new
   * {@code clientId}. Supplying the value of {@code null} will produce an
   * MCC {@code AdWordsUser} from a "client" {@code AdWordsUser}, essentially
   * clearing the {@code clientEmail} or {@code clientCustomerId} fields.
   *
   * @param clientId the client id, either a login email or customer Id, to
   *     generate a new {@code AdWordsUser} for
   * @return a new {@code AdWordsUser} for the client specified
   */
  public AdWordsUser generateClientAdWordsUser(String clientId) {
    AdWordsUser adWordsUser = new AdWordsUser(email, password, clientId, userAgent,
        developerToken, applicationToken, useSandbox);
    adWordsUser.authToken = authToken;
    return adWordsUser;
  }

  /**
   * Gets the email.
   *
   * @return the email
   */
  public String getEmail() {
    return email;
  }

  /**
   * Gets the client email.
   *
   * @return the client email
   */
  public String getClientEmail() {
    return clientEmail;
  }

  /**
   * Gets the client customer Id.
   *
   * @return the client customer Id.
   */
  public String getClientCustomerId() {
    return clientCustomerId;
  }

  /**
   * Gets the password.
   *
   * @return the password
   */
  public String getPassword() {
    return password;
  }

  /**
   * Gets the user-agent.
   *
   * @return the userAgent
   */
  public String getUserAgent() {
    return userAgent;
  }

  /**
   * Gets the developer token.
   *
   * @return the developerToken
   */
  public String getDeveloperToken() {
    return developerToken;
  }

  /**
   * Gets the application token.
   *
   * @return the applicationToken
   * @deprecated Application tokens no longer required
   */
  public String getApplicationToken() {
    return applicationToken;
  }

  /**
   * Returns {@code true} if the user is using the sandbox.
   *
   * @return {@code true} if the user is using the sandbox
   */
  public boolean isUsingSandbox() {
    return useSandbox;
  }

  /**
   * Sets whether all future services will be generated to use the sandbox.
   *
   * @param useSandbox {@code true} if all future services will be generated
   *     to use the sandbox
   */
  public void useSandbox(boolean useSandbox) {
    this.useSandbox = useSandbox;
  }

  /**
   * Returns {@code true} if the this user is configured to use partial
   * failure.
   *
   * @return {@code true} if the this user is configured to use partial failure
   */
  public boolean isUsingPartialFailure() {
    return partialFailure;
  }

  /**
   * Sets if this user is configured to use partial failure.
   *
   * @param usePartialFailure {@code true} if the this user is configured to
   *     use partial failure.
   */
  public void setUsePartialFailure(boolean usePartialFailure) {
    this.partialFailure = usePartialFailure;
  }

  /**
   * Returns {@code true} if the this user is configured to return money in
   * micros for reports.
   *
   * @return {@code true} if the this user is configured to return money in
   *     micros for reports
   */
  public boolean isReportsReturnMoneyInMicros() {
    return returnMoneyInMicros;
  }

  /**
   * Sets if this user is configured to return money in micros for reports.
   *
   * @param returnMoneyInMicros {@code true} if the this user is configured to
   *     return money in micros for reports.
   */
  public void setReportsReturnMoneyInMicros(boolean returnMoneyInMicros) {
    this.returnMoneyInMicros = returnMoneyInMicros;
  }

  /**
   * Gets the registered authentication token associated with this user. This
   * method does not generate a new authentication token; use the
   * {@link AuthToken#getAuthToken()} method to do this. This can be
   * {@code null} if the authentication token was never set.
   *
   * @return the registered authentication token with this user
   */
  public String getRegisteredAuthToken() {
    return authToken;
  }

  /**
   * Sets the authentication token that is retrieved by
   * {@link #getRegisteredAuthToken()}. This authentication token will be used
   * when generating v2009 services; i.e.
   * {@code getService(AdWordsVersion.V2009*.*)}.
   *
   * @param authToken the authentication token to use when generating v2009
   *     services
   */
  public void setAuthToken(String authToken) {
    this.authToken = authToken;
  }

  /**
   * Gets the effective user of the this {@code AdWordsUser}. That is, if a
   * client email or customer Id header is present, it will be considered the
   * effective user and returned. Otherwise, the email header is returned.
   *
   * @return the active user of this {@code AdWordsUser}
   */
  public String getEffectiveUserId() {
    String effectiveUser = email;

    if (clientEmail != null) {
      effectiveUser = clientEmail;
    } else if (clientCustomerId != null) {
      effectiveUser = clientCustomerId;
    }

    return effectiveUser;
  }

  /**
   * Uses the client-config.wsdd located in the classpath instead of loading
   * the runtime {@code EngineConfiguration}, which sets the HTTP
   * transport to {@link AdWordsHttpSender}.
   *
   * @param useClasspathClientConfig {@code true} if the client-config.wsdd
   *     located in the classpath should be used
   */
  public static void useClasspathClientConfig(boolean useClasspathClientConfig) {
    AdWordsUser.useClasspathClientConfig = useClasspathClientConfig;
  }

  /**
   * Returns {@code true} if the {@link AdWordsServiceFactory} should use the
   * client-config.wsdd located in the classpath instead of loading the runtime
   * {@code EngineConfiguration}. See
   * {@link #useClasspathClientConfig(boolean)}.
   *
   * @return {@code true} if the client-config.wsdd located in the classpath
   *     should be used
   */
  public static boolean getUseClasspathClientConfig() {
    return useClasspathClientConfig;
  }

  /**
   * Calling this method with an argument of {@code true} configures the
   * adwords api library to use the Google AppEngine HTTP API when fetching
   * client login information for a user. This is necessary to do for any
   * app running on top of AppEngine.
   * <p>
   * Subsequently calling this method with an argument of {@code false} resets
   * the library to its default behavior.
   */
  public static void useGaeHttpApi(boolean useGaeHttpApi) {
    AdWordsUser.useGaeHttpApi = useGaeHttpApi;
  }

  /**
   * Returns {@code true} if the {@link AdWordsServiceFactory} should use the
   * Google AppEngine HTTP API to fetch client login information.
   */
  public static boolean getUseGaeHttpApi() {
    return useGaeHttpApi;
  }

  /**
   * Gets the client library identifier used for user-agent fields.
   * @return the client library identifier used for user-agent fields
   */
  public String getClientLibraryIdentifier() {
    return libName + " - " + libVersion + " - " + userAgent;
  }

  /**
   * Determines whether clientId is a clientEmail or clientCustomerId.
   *
   * @param clientId The id of the client to determine
   * @return {@code true} if id is an email address
   */
  private boolean isEmail(String clientId) {
    // Determine if clientId is a clientEmail or clientCustomerId
    if (clientId != null) {
      int score = 0;
      for (int index = 0; index < clientId.length(); index++) {
        if (Character.isDigit(clientId.charAt(index))) {
          score++;
        }
      }
      if (score < clientId.length() - 2) {
        return true;
      }
    }

    return false;
  }

  /**
   * Loads the system properties statically.
   */
  private static void loadSystemProperties() {
    if (System.getProperty(USE_CLASSPATH_CLIENT_CONFIG_PROPERTY) != null) {
      useClasspathClientConfig = Boolean.parseBoolean(
          System.getProperty(USE_CLASSPATH_CLIENT_CONFIG_PROPERTY));
    }
    if (System.getProperty(USE_GAE_HTTP_API_PROPERTY) != null) {
      useGaeHttpApi = Boolean.parseBoolean(
          System.getProperty(USE_GAE_HTTP_API_PROPERTY));
    }
  }

  /**
   * Loads the build properties from the build.properties and
   * build-extension.properties file.
   */
  private static void loadBuildProperties() {
    Properties buildProps = new Properties();
    try {
      buildProps = loadResourcePropertiesFile("build.properties");
    } catch (IOException e) {
      AdWordsServiceLogger.AW_API_LIB_LOG.error("Could not load build.properties file.");
    }

    Properties buildExtProps = new Properties();
    try {
      buildExtProps = loadResourcePropertiesFile("build-extension.properties");
    } catch (IOException e) {
      AdWordsServiceLogger.AW_API_LIB_LOG.error("Could not load build-extension.properties file.");
    }

    String libPrefix = (buildProps.containsKey("lib_prefix_name"))
        ? buildProps.getProperty("lib_prefix_name") : DEFAULT_LIB_PREFIX_NAME;
    String libPostfix = (buildProps.containsKey("lib_postfix_name"))
        ? buildProps.getProperty("lib_postfix_name") : DEFAULT_LIB_POSTFIX_NAME;
    String libVersion = (buildProps.containsKey("lib_version"))
        ? buildProps.getProperty("lib_version") : DEFAULT_LIB_VERSION;

    String libExtName = (buildExtProps.containsKey("lib_ext_name"))
        ? buildExtProps.getProperty("lib_ext_name") : "";
    String libExtVersion = (buildExtProps.containsKey("lib_ext_version"))
        ? buildExtProps.getProperty("lib_ext_version") : "";

    AdWordsUser.libName = libPrefix + " " + (libExtName.equals("") ? libExtName : libExtName + " ")
        + libPostfix;
    AdWordsUser.libVersion = "v" + libVersion + libExtVersion;
  }

  /**
   * Loads the properties file from resource relative to the src directory.
   *
   * @param fileName the file from resource to load
   * @return the loaded properties
   * @throws IOException if the properties cannot be loaded
   */
  private static Properties loadResourcePropertiesFile(String fileName) throws IOException {
    Properties properties = new Properties();
    InputStream propertiesFile = null;

    try {
      propertiesFile =
        AdWordsUser.class.getClassLoader().getResourceAsStream(fileName);
      if (propertiesFile != null) {
        properties.load(propertiesFile);
      }
    } finally {
      if (propertiesFile != null) {
        propertiesFile.close();
      }
    }

    return properties;
  }

  /**
   * Returns {@code Properties} object populated with properties loaded
   * from a given file.
   *
   * @param fileName absolute or relative path to the properties file, which
   *     has to follow {@code java.util.Properties} file format
   * @return Map object with loaded credentials
   * @throws IOException if problem occurred while reading file
   */
  public static Map getMap(String fileName) throws IOException {
    Properties credentials = new Properties();
    credentials.load(new FileInputStream(fileName));
    return credentials;
  }

  /**
   * Gets the clientId from the properties in either the form of clientId,
   * clientEmail, or clientCustomerId.
   *
   * @param properties the properties containing the user credentials
   * @return the client id to use or {@code null} if none found
   */
  private static String getClientIdFromProperties(Map properties) {
    if (properties.get("clientId") != null) {
      return (String) properties.get("clientId");
    } else  if (properties.get("clientEmail") != null) {
      return (String) properties.get("clientEmail");
    } else if (properties.get("clientCustomerId") != null) {
      return (String) properties.get("clientCustomerId");
    } else {
      return null;
    }
  }
}
